package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"regexp"
	"sort"
	"strings"

	"golang.org/x/net/html"

	"gitea.com/lunny/html2md"
)

type Colour struct {
	Reset  string
	Red    string
	Green  string
	Yellow string
	Blue   string
	Purple string
	Cyan   string
	Gray   string
	White  string
}

type Honk struct {
	ID     int
	What   string
	Handle string
	Oondle string
	XID    string
	Date   string
	Noise  string
	Donks  []struct {
		URL string
	}
}

type HonkSet struct {
	Honks []Honk
}

func isPgDn(b []byte) bool {
	return len(b) == 4 && b[0] == 27 && b[1] == 91 && b[2] == 54 && b[3] == 126
}

func dataHome() string {
	xdgDataHome, _ := os.LookupEnv("XDG_DATA_HOME")
	if xdgDataHome == "" {
		user, err := os.UserHomeDir()
		if err != nil {
			panic(err)
		}
		xdgDataHome = user + "/.local/share"
	}
	return xdgDataHome + "/gonk"
}

func configHome() string {
	xdgConfigHome, _ := os.LookupEnv("XDG_CONFIG_HOME")
	if xdgConfigHome == "" {
		user, err := os.UserHomeDir()
		if err != nil {
			panic(err)
		}
		xdgConfigHome = user + "/.config"
	}
	return xdgConfigHome + "/gonk"
}

func setUpColour(colourful bool) Colour {
	if colourful {
		return Colour{
			Reset:  "\033[0m",
			Red:    "\033[31m",
			Green:  "\033[32m",
			Yellow: "\033[33m",
			Blue:   "\033[34m",
			Purple: "\033[35m",
			Cyan:   "\033[36m",
			Gray:   "\033[37m",
			White:  "\033[97m",
		}
	} else {
		return Colour{}
	}
}

func gettoken(server, username, password string) string {
	form := make(url.Values)
	form.Add("username", username)
	form.Add("password", password)
	form.Add("gettoken", "1")
	loginurl := fmt.Sprintf("https://%s/dologin", server)
	req, err := http.NewRequest("POST", loginurl, strings.NewReader(form.Encode()))
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	answer, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	if resp.StatusCode != 200 {
		log.Fatalf("status: %d: %s", resp.StatusCode, answer)
	}

	return string(answer)
}

func logout(server, token string) {
	apiurl := fmt.Sprintf("https://%s/logout", server)
	req, err := http.NewRequest("GET", apiurl, nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Add("Authorization", "Bearer "+token)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		answer, _ := ioutil.ReadAll(resp.Body)
		log.Fatalf("status: %d: %s", resp.StatusCode, answer)
	}
}

func gethonks(server, token string, after int) HonkSet {
	form := make(url.Values)
	form.Add("action", "gethonks")
	form.Add("page", "home")
	form.Add("after", fmt.Sprintf("%d", after))
	apiurl := fmt.Sprintf("https://%s/api?%s", server, form.Encode())
	req, err := http.NewRequest("GET", apiurl, nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Add("Authorization", "Bearer "+token)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		answer, _ := ioutil.ReadAll(resp.Body)
		log.Fatalf("status: %d: %s", resp.StatusCode, answer)
	}
	var honks HonkSet
	d := json.NewDecoder(resp.Body)
	err = d.Decode(&honks)
	if err != nil {
		log.Fatal(err)
	}
	return honks
}

func readkey() []byte {
	exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run()
	exec.Command("stty", "-F", "/dev/tty", "-echo").Run()
	defer exec.Command("stty", "-F", "/dev/tty", "echo").Run()
	var b []byte = make([]byte, 4)
	os.Stdin.Read(b)
	return b
}

func showHonks(honks HonkSet, after int, colour Colour) (int, bool) {
	broken := false
	honksNumber := len(honks.Honks)
	fmt.Println(colour.Cyan + "## o to open in browser, n/Space/PgDn for next, d to show donks, q to exit " + colour.Reset)
	for i, honk := range honks.Honks {
		md := html2md.Convert(honk.Noise)
		md = html.UnescapeString(md)
		md = changeA(md, colour)
		head := fmt.Sprintf(colour.White+"%d/%d\n%s %s %s (%s) \n%s\n"+colour.Reset, i+1, honksNumber, honk.Handle, honk.What, honk.Oondle, honk.Date, honk.XID)
		head = strings.ReplaceAll(head, "  ", " ")
		fmt.Println(head)
		fmt.Printf("%s\n\n", md)
		if len(honk.Donks) > 0 {
			fmt.Println(colour.White + "## has donks" + colour.Reset)
		}
		for {
			key := readkey()
			if rune(key[0]) == 'n' || rune(key[0]) == ' ' || isPgDn(key) {
				after = honk.ID
				fmt.Printf("\n\n")
				break
			}
			if rune(key[0]) == 'o' {
				exec.Command("xdg-open", honk.XID).Run()
				continue
			}
			if rune(key[0]) == 'd' {
				for i, donk := range honk.Donks {
					fmt.Printf(colour.White+"Donk #%d: %s\n"+colour.Reset, i, donk.URL)
				}
			}
			if rune(key[0]) == 'q' {
				broken = true
				return after, broken
			}
		}
	}
	return after, broken
}

func changeA(noise string, colour Colour) string {
	reg := regexp.MustCompile("\\[([^]]+)\\]\\(([^)]+)\\)")
	indexes := reg.FindAllStringSubmatchIndex(noise, -1)
	last := 0
	renoise := ""
	for _, index := range indexes {
		s, t, s1, t1, s2, t2 := index[0], index[1], index[2], index[3], index[4], index[5]
		bf := noise[last:s]
		label := noise[s1:t1]
		href := noise[s2:t2]
		last = t
		if label == href {
			renoise = renoise + bf + colour.Blue + href + colour.Reset
		} else if label[0] == '#' {
			renoise = renoise + bf + colour.Green + label + colour.Reset
		} else if label[0] == '@' {
			instance, err := url.Parse(href)
			if err != nil {
				renoise = renoise + bf + "[" + colour.Green + label + colour.Reset + "](" + colour.Blue + href + colour.Reset + ")"
			} else {
				renoise = renoise + bf + colour.Green + label + "@" + instance.Host + colour.Reset
			}
		} else {
			renoise = renoise + bf + "[" + colour.Green + label + colour.Reset + "](" + colour.Blue + href + colour.Reset + ")"
		}
	}
	renoise = renoise + noise[last:]
	return renoise
}

func main() {
	server := ""
	username := ""
	password := ""
	after := 0
	broken := false

	colourful := true
	if len(os.Args) > 1 && os.Args[1] == "-t" {
		colourful = false
	}
	colour := setUpColour(colourful)

	err := os.MkdirAll(dataHome(), 0755)
	if err != nil {
		fmt.Println("[ERR] while making home directory: %v", err)
		os.Exit(1)
	}

	data, err := ioutil.ReadFile(configHome() + "/account")
	if err != nil {
		fmt.Printf("[ERR] cannot open account file (%s): %v\n", configHome()+"/account", err)
		os.Exit(1)
	}
	for _, line := range strings.Split(string(data), "\n") {
		if !strings.Contains(line, "=") {
			continue
		}
		kv := strings.Split(line, "=")
		k := strings.Trim(kv[0], " ")
		v := strings.Trim(kv[1], " ")
		switch k {
		case "username":
			username = v
		case "password":
			password = v
		case "server":
			server = v
		}
	}
	if server == "" || username == "" || password == "" {
		log.Println("[ERR] file 'account' with username, password, and server needed")
		os.Exit(1)
	}

	token := gettoken(server, username, password)
	data, err = ioutil.ReadFile(dataHome() + "/after")
	if err != nil {
		log.Printf("[WARN] file reading error: %s\nShowing all honks", err)
	}
	fmt.Sscanf(string(data), "%d", &after)
	honks := gethonks(server, token, after)
	sort.Slice(honks.Honks, func(i, j int) bool {
		return honks.Honks[i].ID < honks.Honks[j].ID
	})
	after, broken = showHonks(honks, after, colour)
	if !broken {
		fmt.Println(colour.Cyan + "## No more honks" + colour.Reset)
	}
	ioutil.WriteFile(dataHome()+"/after", []byte(fmt.Sprintf("%d", after)), 0644)
	logout(server, token)
}
